package com.xormedia.httpservice;

import com.xormedia.httpservice.model.ServiceArgs;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import lombok.extern.slf4j.Slf4j;

import javax.activation.MimetypesFileTypeMap;
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.math.RoundingMode;
import java.net.URLDecoder;
import java.nio.channels.FileChannel;
import java.nio.file.StandardOpenOption;
import java.text.DecimalFormat;
import java.util.regex.Pattern;

/**
 * com.xormedia.httpservice.HttpRequestHandler
 *
 * @author zhikang.peng
 * @date 2017年6月20日上午10:36:10
 * @since 1.0
 */
@Slf4j
public class HttpRequestHandler extends SimpleChannelInboundHandler<FullHttpRequest> {

  private static final Pattern INSECURE_URI = Pattern.compile(".*[<>&\"].*");
  private static final Pattern ALLOWED_FILE_NAME = Pattern.compile("[A-Za-z0-9][-_A-Za-z0-9.]*");

  private ServiceArgs serviceArgs;

  public HttpRequestHandler(ServiceArgs serviceArgs) {
    this.serviceArgs = serviceArgs;
  }

  @Override
  protected void channelRead0(ChannelHandlerContext ctx, FullHttpRequest request) throws Exception {
    log.info("Access Path {}", request.uri());
    if (!request.decoderResult().isSuccess() || request.method() != HttpMethod.GET) {
      sendError(ctx, HttpResponseStatus.BAD_REQUEST);
      return;
    }
    final String uri = request.uri();
    final String path = sanitizeUri(uri);
    if (path == null) {
      sendError(ctx, HttpResponseStatus.FORBIDDEN);
      return;
    }
    File file = new File(serviceArgs.getBasePath(), path);
    if (file.isHidden() || !file.exists()) {
      sendError(ctx, HttpResponseStatus.NOT_FOUND);
      return;
    }
    if (file.isHidden() || !file.exists() || file.isDirectory()) {
      sendError(ctx, HttpResponseStatus.NOT_FOUND);
      return;
    }
    FileChannel fileChannel = FileChannel.open(file.toPath(), StandardOpenOption.READ);
    long fileLength = fileChannel.size();
    HttpResponse response = new DefaultHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.OK);

    HttpUtil.setContentLength(response, fileLength);
    setContentTypeHeader(response, file);
    if (HttpUtil.isKeepAlive(request)) {
      response.headers().set(HttpHeaderNames.CONNECTION, HttpHeaderValues.KEEP_ALIVE);
    }
    ctx.write(response);

    // 默认走这个, 会调用fileChannel#transferTo()零拷贝
    ChannelFuture sendFileFuture = ctx.write(new DefaultFileRegion(fileChannel, 0, fileChannel.size()), ctx.newProgressivePromise());
    // Write the end marker.
    ChannelFuture lastContentFuture = ctx.writeAndFlush(LastHttpContent.EMPTY_LAST_CONTENT);

    DecimalFormat df = new DecimalFormat("0.##");
    df.setRoundingMode(RoundingMode.DOWN);

    sendFileFuture.addListener(new ChannelProgressiveFutureListener() {
      private long preProgress;

      @Override
      public void operationProgressed(ChannelProgressiveFuture channelProgressiveFuture, long progress, long total) {
        if (total < 0) {
          log.info("Transfer progress: {}", progress);
        } else if (preProgress != progress) {
          float rate = progress / (float) total * 100;
          String rateText = df.format(rate);
          log.info("Transfer progress: {}/{}, {}%", progress, total, rateText);
        }
        this.preProgress = progress;
      }

      @Override
      public void operationComplete(ChannelProgressiveFuture channelProgressiveFuture) {
        log.info("Transfer complete.");
        try {
          if (fileChannel.isOpen()) {
            fileChannel.close();
          }
        } catch (IOException ignored) {
        }
      }
    });

    if (!HttpUtil.isKeepAlive(request)) {
      lastContentFuture.addListener(ChannelFutureListener.CLOSE);
    }

  }

  /**
   * 对url进行编码校验
   *
   * @param uri 访问对url
   * @return 返回校验后对路径
   */
  private String sanitizeUri(String uri) {
    try {
      //对URL进行编码
      uri = URLDecoder.decode(uri, CharsetUtil.UTF_8.name());
    } catch (UnsupportedEncodingException e) {
      try {
        uri = URLDecoder.decode(uri, CharsetUtil.ISO_8859_1.name());
      } catch (UnsupportedEncodingException e2) {
        throw new Error(e2.getLocalizedMessage());
      }
    }
    if (!uri.startsWith("/")) {
      return null;
    }

    uri = uri.replace('/', File.separatorChar);
    return uri;
  }


  private static void sendRedirect(ChannelHandlerContext ctx, String newUrl) {
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, HttpResponseStatus.FOUND);
    response.headers().set(HttpHeaderNames.LOCATION, newUrl);
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  private static void sendError(ChannelHandlerContext ctx, HttpResponseStatus status) {
    FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer("Failure: " + status + "\r\n", CharsetUtil.UTF_8));
    response.headers().set(HttpHeaderNames.CONTENT_TYPE, HttpHeaderValues.TEXT_PLAIN);
    ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
  }

  private static void setContentTypeHeader(HttpResponse response, File file) {
    MimetypesFileTypeMap mimetypesFileTypeMap = new MimetypesFileTypeMap();
    response.headers().set(HttpHeaderNames.CONTENT_TYPE, mimetypesFileTypeMap.getContentType(file.getParent()));
  }

}
